library GetProc initializer Init
//===========================================================================
// Information:
//==============
//
//      GetProc allows you to smooth out the streaky behavior of random procs.
//  GetRandomReal() often produces long strings of high or low values, which
//  may result in a relatively rare proc occuring many times in a row. While
//  using GetProc, each time a proc occurs it becomes less likely, and each
//  time a proc fails to occur it becomes more likely. The overall chance of
//  the proc occuring remains the same, but long streaks of the same result
//  become far less likely.
//
//      By adjusting the Weight value, you can control the extent to which
//  streaky behavior curtailed. At a low Weight value, it will produce a
//  series of results that resembles random results, but without the streaky
//  behavior of WC3's random number generator. At very high weight values,
//  procs will occur roughly every 1 / ProcChance attempts, although a dec-
//  ent amount of variation will remain. For most maps, weight values between
//  0.25 and 0.75 should be acceptable.
//
//===========================================================================
// How to use GetProc:
//=====================
//
//      Since proc chances vary on a per-event basis, you must specify which
//  event you want a proc for using a unique integer. The easiest way to get
//  a unique integer is to declare a key, shown in the example below. (It would
//  also be possible to use the rawcode of a spell.)
//
//      Procs are typically associated with specific units. For example: you
//  want to determine whether a particular unit scored a critical hit or not.
//  First you'd need a unique key representing critical hits. Then you would
//  call GetProc(key, unit, chance) to determine whether the crit occured:
//
//  globals
//      key CriticalHit
//  endglobals
//
//  function OnDamage takes nothing returns nothing
//      if GetProc(CriticalHit, GetEventDamageSource(), CritChance) then
//          call DoCriticalHit()
//      endif
//  endfunction
//
//      Some procs may not be associated with a specific unit. For example: every
//  10 seconds there is a chance to spawn a group of monsters. First, you would need a
//  declare key for that specific event. Then you would then call GetProc(key, null,
//  chance) to get a boolean indicating whether the spawn occured:
//
//  globals
//      key SpawnMonsters
//  endglobals
//
//  function EveryTenSeconds takes nothing returns nothing
//      if GetProc(SpawnMonsters, null, SpawnChance) then
//          call DoSpawnMonsters()
//      endif
//  endfunction
//
//===========================================================================
// GetProc API:
//==============
//
//  GetProc(key, unit, chance) -> boolean :
//        This function requires three parameters and returns a boolean that
//    indicates whether a particular proc occured or not. The first parameter
//    must be a unique integer value that represets the proc type. The second
//    is the particular unit you're getting a proc for; you may use null if
//    the proc is not associated with a unit. The final parameter is the base
//    chance value of the proc occuring.
//
//  GetProcChance(key, unit, chance) -> real :
//        This function takes the same parameters as GetProc, and returns the
//    percentage chance that the proc would currently occur.
//
//  SetProcWeight(key, weight) :
//        This function allows you to override the default Weight value for
//    the proc associated with a particular key.
//
//  ResetProcsForUnit(unit) :
//        This function resets the percentage chance of all procs associated
//    with this unit occuring.
//
//  ResetAllProcs() :
//        This function resets the percentage chance of all procs occuring.
//
//===========================================================================
// Configuration:
//================

globals
    private constant real Weight = 0.5
    //Set this to a value between 0 and 1. At 0, results are not affected
    //at all. At 1, streaky results are very limited. Leaving the weight
    //at 0.5 should be a good compromise for most maps.
    private constant integer MaxProcs = 8190
    //You should leave this alone unless you happen to get an error message.
    //It should be impossible to run out of Proc instances unless you leak
    //handle references. If you do run out, increase this value as needed.
endglobals

//===========================================================================

globals
    private hashtable ht = InitHashtable()
    private key Weights
    private key Resets
endglobals

private struct Proc [MaxProcs]
    static integer allresets = 0
    integer reset = 0
    integer allreset = 0
    real trues = 0.
    real falses = 0.
    real oldchance = 0.
endstruct

function GetProc takes integer key, unit u, real chance returns boolean
    local integer id = GetHandleId(u)
    local real weight = Weight
    local Proc p
        if not HaveSavedInteger(ht, key, id) then
            call SaveInteger(ht, key, id, Proc.create())
        endif
        set p = LoadInteger(ht, key, id)
        if p == 0 then
            call BJDebugMsg("GetProc warning: Ran out of Proc instances. Increase MaxProcs in the configuration section.")
            return false
        endif
        if p.allreset < Proc.allresets then
            set p.allreset = Proc.allresets
            set p.trues = 0
            set p.falses = 0
        endif
        if p.reset < LoadInteger(ht, Resets, id) then
            set p.reset = LoadInteger(ht, Resets, id)
            set p.trues = 0
            set p.falses = 0
        endif
        if chance != p.oldchance then
            set p.oldchance = chance
            set p.trues = 0
            set p.falses = 0
        endif
        if chance >= 1. then
            set p.trues = p.trues + 1
            return true
        elseif chance <= 0. then
            set p.falses = p.falses + 1
            return false
        endif
        if HaveSavedReal(ht, Weights, key) then
            set weight = LoadReal(ht, Weights, key)
        endif
        set weight = weight * 0.5 * RMinBJ(chance, 1. - chance)
        if GetRandomReal(0., 1.) <= chance - (p.trues*weight) / chance + (p.falses*weight) / (1. - chance) then
            set p.trues = p.trues + 1.
            return true
        endif
        set p.falses = p.falses + 1.
    return false
endfunction

function GetProcChance takes integer key, unit u, real chance returns real
    local Proc p = LoadInteger(ht, key, GetHandleId(u))
    local real weight = Weight
        if p == 0 then
            return chance
        elseif p.allreset < Proc.allresets then
            return chance
        elseif p.reset < LoadInteger(ht, Resets, GetHandleId(u)) then
            return chance
        elseif chance != p.oldchance then
            return chance
        elseif chance >= 1. then
            return 1.
        elseif chance <= 0. then
            return 0.
        endif
        if HaveSavedReal(ht, Weights, key) then
            set weight = LoadReal(ht, Weights, key)
        endif
        set weight = weight * 0.5 * RMinBJ(chance, 1. - chance)
    return chance - (p.trues*weight) / chance + (p.falses*weight) / (1. - chance)
endfunction

function SetProcWeight takes integer key, real weight returns nothing
    if weight == Weight then
        call RemoveSavedReal(ht, Weights, key)
    endif
    call SaveReal(ht, Weights, key, weight)
endfunction

function ResetProcsForUnit takes unit u returns nothing
    call SaveInteger(ht, Resets, GetHandleId(u), LoadInteger(ht, Resets, GetHandleId(u)) + 1)
endfunction

function ResetAllProcs takes nothing returns nothing
    set Proc.allresets = Proc.allresets + 1
endfunction

//===========================================================================

private function UnitEntersMap takes nothing returns boolean
        call ResetProcsForUnit(GetFilterUnit())
    return false
endfunction

private function Init takes nothing returns nothing
    local trigger t = CreateTrigger()
    local region maparea = CreateRegion()
        call RegionAddRect(maparea, bj_mapInitialPlayableArea)
        call TriggerRegisterEnterRegion(t, maparea, Condition(function UnitEntersMap))
endfunction

endlibrary

